/**
 * 
 */
package fr.utbm.gi.vi51.slizers.pw.environment;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import javax.vecmath.Point2i;

import fr.utbm.gi.vi51.slizers.framework.agent.AgentBody;
import fr.utbm.gi.vi51.slizers.framework.agent.Direction;
import fr.utbm.gi.vi51.slizers.framework.environment.AbstractEnvironment;
import fr.utbm.gi.vi51.slizers.framework.environment.EnvironmentEvent;
import fr.utbm.gi.vi51.slizers.framework.environment.InfluenceCollector;
import fr.utbm.gi.vi51.slizers.framework.environment.InfluenceConflictSolver;
import fr.utbm.gi.vi51.slizers.framework.environment.PerceptionGenerator;
import fr.utbm.gi.vi51.slizers.framework.schedule.SimulationTimeManager;
import fr.utbm.gi.vi51.slizers.framework.util.Vector2i;
import fr.utbm.gi.vi51.slizers.pw.agent.CreatureBody;
import fr.utbm.gi.vi51.slizers.pw.agent.CreatureBodyState;
import fr.utbm.gi.vi51.slizers.pw.agent.action.CreatureAction;
import fr.utbm.gi.vi51.slizers.pw.agent.action.CreatureActionInfluence;

/**
 * Implementation of a environmental world.
 * 
 * @author Tarik Belabbas
 * 
 */
public class World extends AbstractEnvironment implements WorldStateProvider,
		WorldMapProvider {	
	/**
	 * The map of the world.
	 */
	private List<List<WorldElement>> worldMap = null;
	
	/**
	 * The entrance of the map.
	 */
	private Point2i mapEntrance = null;
	
	/**
	 * The exit of the map.
	 */
	private Point2i mapExit = null;

	/**
	 * The fatal fall height for creatures.
	 */
	private final int FATAL_FALL_DISTANCE = 5;
	
	/**
	 * Parameterized constructor.
	 * 
	 * @param _stepDuration
	 *            the duration of one simulation step in milliseconds
	 */
	public World(float _stepDuration) {
		super(new SimulationTimeManager(_stepDuration));
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see fr.utbm.gi.vi51.slizers.framework.environment.AbstractEnvironment#
	 * createInfluenceCollector()
	 */
	@Override
	protected InfluenceCollector< World > createInfluenceCollector() {
		return new WorldInfluenceCollector(this);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see fr.utbm.gi.vi51.slizers.framework.environment.AbstractEnvironment#
	 * createInfluenceConflictSolver()
	 */
	@Override
	protected InfluenceConflictSolver< World > createInfluenceConflictSolver() {
		return new WorldInfluenceConflictSolver(this);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see fr.utbm.gi.vi51.slizers.framework.environment.AbstractEnvironment#
	 * createPerceptionGenerator()
	 */
	@Override
	protected PerceptionGenerator< World > createPerceptionGenerator() {
		return new WorldPerceptionGenerator(this);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see fr.utbm.gi.vi51.slizers.framework.environment.AbstractEnvironment#
	 * createStateEvent()
	 */
	@Override
	protected EnvironmentEvent createStateEvent() {
		return new WorldStateEvent(this);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see fr.utbm.gi.vi51.slizers.pw.environment.WorldStateProvider#getState()
	 */
	@Override
	public WorldState getState() {
		return new WorldState(cloneAgentBodies());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see fr.utbm.gi.vi51.slizers.framework.environment.Environment#getWidth()
	 */
	@Override
	public int getWidth() {
		return this.worldMap.get(0).size();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * fr.utbm.gi.vi51.slizers.framework.environment.Environment#getHeight()
	 */
	@Override
	public int getHeight() {
		return this.worldMap.size();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see fr.utbm.gi.vi51.slizers.pw.environment.WorldMapProvider#getMap()
	 */
	@Override
	public List<List<WorldElement>> getMap() {
		return this.worldMap;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see fr.utbm.gi.vi51.slizers.framework.environment.AbstractEnvironment#
	 * getStartingPosition()
	 */
	@Override
	protected Point2i getStartingPosition()
	{
		return this.mapEntrance;
	}

	/**
	 * Sets the map of the world.
	 * 
	 * @param _worldMap the new map of the world
	 */
	public void setMap( List< List< WorldElement >> _worldMap,
			Point2i _mapEntrance, Point2i _mapExit )
	{
		if ( _worldMap != null )
		{
			this.worldMap = _worldMap;
			this.mapEntrance = _mapEntrance;
			this.mapExit = _mapExit;

			notifyListeners( new WorldMapEvent( this ) );
		}
	}

	/**
	 * Returns a relation position from a position.
	 * 
	 * @param _position the reference position
	 * @param _relative the relative position
	 */
	private Point2i getRelativePosition( Point2i _position,	RelativePosition _relative )
	{
		return new Point2i( _position.x + _relative.dx, _position.y + _relative.dy );
	}
	
	/**
	 * Sets the world element at a relation position from a position.
	 * 
	 * @param _position the reference position
	 * @param _element the new element
	 */
	private void setWorldElement( Point2i _position, WorldElement _element )
	{
		this.worldMap.get( _position.x ).set( _position.y, _element );
	}
	
	/**
	 * Returns the world element at a position.
	 * 
	 * @param _position the reference position
	 */
	private WorldElement getWorldElement( Point2i _position )
	{
		return this.worldMap.get( _position.x ).get( _position.y );
	}
	
	/**
	 * Returns the world element at a position.
	 * 
	 * @param _position the reference position
	 */
	private WorldElement getWorldElement( int _positionX, int _positionY )
	{
		return this.worldMap.get( _positionX ).get( _positionY );
	}
	
	/**
	 * Sets the world element at a relative position from a position.
	 * 
	 * @param _position the reference position
	 * @param _relative the relative position of the world element
	 * @param _element the new element
	 */
	private void setRelativeWorldElement( Point2i _position,
			RelativePosition _relative, WorldElement _element )
	{
		this.worldMap.get( _position.x + _relative.dx ).set( _position.y + _relative.dy, _element );
	}
	
	/**
	 * Returns the world element at a relative position from a position.
	 * 
	 * @param _position the reference position
	 * @param _relative the relative position of the world element
	 */
	public WorldElement getRelativeWorldElement( Point2i _position,
			RelativePosition _relative )
	{
		return this.worldMap.get( _position.x + _relative.dx ).get( _position.y + _relative.dy );
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see fr.utbm.gi.vi51.slizers.framework.environment.AbstractEnvironment#
	 * applyInfluences(java.util.Collection,
	 * fr.utbm.gi.vi51.slizers.framework.schedule.SimulationTimeManager)
	 */
	@Override
	protected void applyInfluences( Collection<CreatureActionInfluence> _creatureActionInfluence,
			SimulationTimeManager _timeManager)
	{
		List< WorldEffect > worldEffects = new ArrayList< WorldEffect >( _creatureActionInfluence.size() );

		// Check influences
		for ( CreatureActionInfluence influence : _creatureActionInfluence )
		{
			AgentBody body = getAgentBodyFor( influence.getEmitter() );
			if ( ( body != null ) && ( body instanceof CreatureBody ) )
			{
				CreatureBody creatureBody	= ( CreatureBody )body;
				Point2i position			= creatureBody.getPosition();
				CreatureAction action		= influence.getAction();
				CreatureBodyState state		= creatureBody.getState();
				
				switch ( state )
				{
					case EXITTING:
						{
							exitCreature( creatureBody );
						}
						break;
					
					case BURNING:
						{
							killCreature( creatureBody );
						}
						break;

					case SINKING:
						{
							killCreature( creatureBody );
						}
						break;

					case GUTS_ON_THE_FLOOR:
						{
							killCreature( creatureBody );
						}
						break;
						
					case CLIMBING:
						{
							if ( action == CreatureAction.CLIMB_FORWARD )
							{
								Direction direction			= creatureBody.getDirection();
								RelativePosition relative	= ( direction == Direction.LEFT ) ? RelativePosition.LEFT : RelativePosition.RIGHT;
								RelativePosition relativeAbove	= ( direction == Direction.LEFT ) ? RelativePosition.ABOVE_LEFT : RelativePosition.ABOVE_RIGHT;
								WorldElement forward		= getRelativeWorldElement( position, relative );
								WorldElement above			= getRelativeWorldElement( position, RelativePosition.ABOVE );
								WorldElement aboveForward	= getRelativeWorldElement( position, relativeAbove );
							
								if ( ( forward == WorldElement.ROCK || forward == WorldElement.EARTH )
										&& ( above != WorldElement.ROCK && above != WorldElement.EARTH ) )
								{
									creatureBody.setState( CreatureBodyState.CLIMBING );
									
									if ( aboveForward != WorldElement.ROCK && aboveForward != WorldElement.EARTH )
									{
										worldEffects.add( new WorldMoveEffect( creatureBody, new Vector2i( direction.dx, 1 ) ) );
									}
									else
									{
										worldEffects.add( new WorldMoveEffect( creatureBody, new Vector2i( 0, 1 ) ) );
									}
								}
							}
							else
							{
								creatureBody.setState( CreatureBodyState.FALLING );
							}
						}
						break;
						
					case ON_FLOOR:
					{
						switch( action )
						{
							case NONE:
								{
									// Nothing to do
								}
								break;

							case DIG_AND_MOVE_FORWARD:
								{
									Direction direction = creatureBody.getDirection();
									RelativePosition relative = ( direction == Direction.LEFT ) ? RelativePosition.LEFT : RelativePosition.RIGHT; 
									WorldElement forward = getRelativeWorldElement( position, relative );
									if ( forward == WorldElement.EARTH )
									{
										worldEffects.add( new WorldMoveEffect( creatureBody, new Vector2i( direction.dx, 0 ) ) );
										worldEffects.add( new WorldDestroyEffect( getRelativePosition( position, relative ) ) );
									}
								}
								break;
							
								
							case DIG_AND_MOVE_BOTTOM:
								{
									WorldElement below = getRelativeWorldElement( position, RelativePosition.BELOW );
									if ( below == WorldElement.EARTH )
									{
										worldEffects.add( new WorldMoveEffect( creatureBody, new Vector2i( 0, -1 ) ) );
										worldEffects.add( new WorldDestroyEffect( getRelativePosition( position, RelativePosition.BELOW ) ) );
									}
								}
								break;
								
							case MOVE_FORWARD:
								{
									Direction direction = creatureBody.getDirection();
									RelativePosition relative = ( direction == Direction.LEFT ) ? RelativePosition.LEFT : RelativePosition.RIGHT; 
									WorldElement forward = getRelativeWorldElement( position, relative );
									if ( forward != WorldElement.ROCK && forward != WorldElement.EARTH )
									{
										worldEffects.add( new WorldMoveEffect( creatureBody, new Vector2i( direction.dx, 0 ) ) );
									}
								}
								break;
								
							case TURN_AROUND:
								{
									creatureBody.setDirection( creatureBody.getDirection().opposite() );
								}
							break;
								
							case CLIMB_FORWARD:
								{
									Direction direction			= creatureBody.getDirection();
									RelativePosition relative	= ( direction == Direction.LEFT ) ? RelativePosition.LEFT : RelativePosition.RIGHT;
									RelativePosition relativeAbove	= ( direction == Direction.LEFT ) ? RelativePosition.ABOVE_LEFT : RelativePosition.ABOVE_RIGHT;
									WorldElement forward		= getRelativeWorldElement( position, relative );
									WorldElement above			= getRelativeWorldElement( position, RelativePosition.ABOVE );
									WorldElement aboveForward	= getRelativeWorldElement( position, relativeAbove );
								
									if ( ( forward == WorldElement.ROCK || forward == WorldElement.EARTH )
											&& ( above != WorldElement.ROCK && above != WorldElement.EARTH ) )
									{
										creatureBody.setState( CreatureBodyState.CLIMBING );
										
										if ( aboveForward != WorldElement.ROCK && aboveForward != WorldElement.EARTH )
										{
											worldEffects.add( new WorldMoveEffect( creatureBody, new Vector2i( direction.dx, 1 ) ) );
										}
										else
										{
											worldEffects.add( new WorldMoveEffect( creatureBody, new Vector2i( 0, 1 ) ) );
										}
									}
								}
								break;
								
							case UMBRELLA_OPEN:
								{
									// Nothing to do
								}
								break;
								
							default:
								break;
						}
					}
						break;

					case FALLING:
					{
						boolean fatalFall = ( creatureBody.getFallingDistance() >= this.FATAL_FALL_DISTANCE );
						WorldElement below = getRelativeWorldElement( position, RelativePosition.BELOW );
						
						if ( fatalFall )
						{
							// Fall and die
							if ( below == WorldElement.EARTH || below == WorldElement.ROCK )
							{
								creatureBody.setState( CreatureBodyState.GUTS_ON_THE_FLOOR );
							}
							else
							{
								// Fall but decide to open umbrella
								if ( action == CreatureAction.UMBRELLA_OPEN )
								{
									creatureBody.resetFallingDistance();
									creatureBody.setState( CreatureBodyState.GLIDING );
									worldEffects.add( new WorldMoveEffect( creatureBody, new Vector2i( 0, -1 ) ) );
								}
								else // Fall and continue to fall
								{
									creatureBody.incrementFallingDistance();
									worldEffects.add( new WorldMoveEffect( creatureBody, new Vector2i( 0, -1 ) ) );
								}
							}
						}
						else
						{
							// Fall and land
							if ( below == WorldElement.EARTH || below == WorldElement.ROCK )
							{
								creatureBody.resetFallingDistance();
								creatureBody.setState( CreatureBodyState.ON_FLOOR );
							}
							else
							{
								// Fall but decide to open umbrella
								if ( action == CreatureAction.UMBRELLA_OPEN )
								{
									creatureBody.resetFallingDistance();
									creatureBody.setState( CreatureBodyState.GLIDING );
									worldEffects.add( new WorldMoveEffect( creatureBody, new Vector2i( 0, -1 ) ) );
								}
								else // Fall and continue to fall
								{
									creatureBody.incrementFallingDistance();
									worldEffects.add( new WorldMoveEffect( creatureBody, new Vector2i( 0, -1 ) ) );
								}
							}
						}
					}
						break;

					case GLIDING:
					{
						WorldElement below = getRelativeWorldElement( position, RelativePosition.BELOW );
						
						// Glide and land
						if ( below == WorldElement.EARTH || below == WorldElement.ROCK )
						{
							creatureBody.setState( CreatureBodyState.ON_FLOOR );
						}
						else
						{
							// Glide and keep gliding
							if ( action == CreatureAction.UMBRELLA_OPEN
									|| action == CreatureAction.NONE )
							{
								worldEffects.add( new WorldMoveEffect( creatureBody, new Vector2i( 0, -1 ) ) );
							}
							else // Glide and close umbrella, start to fall
							{
								creatureBody.setState( CreatureBodyState.FALLING );
								creatureBody.incrementFallingDistance();
								worldEffects.add( new WorldMoveEffect( creatureBody, new Vector2i( 0, -1 ) ) );
							}
						}
					}
						break;

					default:
						break;
				}
			}
		}
		
		// Apply effects on the world
		for ( WorldEffect effect : worldEffects )
		{
			effect.applyEffect();
		}
		
		// Set effect on body on special cells
		for ( CreatureActionInfluence influence : _creatureActionInfluence )
		{
			AgentBody body = getAgentBodyFor( influence.getEmitter() );
			if ( ( body != null ) && ( body instanceof CreatureBody ) )
			{
				CreatureBody creatureBody	= ( CreatureBody )body;
				Point2i position			= creatureBody.getPosition();
				WorldElement cell			= getWorldElement( position );
				
				if ( creatureBody.getState() == CreatureBodyState.ON_FLOOR  && cell != WorldElement.FIRE && cell != WorldElement.WATER && cell != WorldElement.EXIT)
				{
					WorldElement below = getRelativeWorldElement( position, RelativePosition.BELOW );
					if ( below != WorldElement.ROCK && below != WorldElement.EARTH)
					{
						creatureBody.setState( CreatureBodyState.FALLING );
					}
				}
				else
				{
					switch ( cell )
					{
						case WATER:
							creatureBody.setState( CreatureBodyState.SINKING );
							break;
							
						case FIRE:
							creatureBody.setState( CreatureBodyState.BURNING );
							break;
							
						case EXIT:
							creatureBody.setState( CreatureBodyState.EXITTING );
							break;
							
						default:
							break;
					}
				}
				
			}
		}
	}
	
	/**
	 * Kills a creature of the world.
	 * 
	 * @param _creatureBody the body of the creature
	 */
	private void killCreature( CreatureBody _creatureBody )
	{
		killAgentBody( _creatureBody.getOwner() );
		_creatureBody.setState( CreatureBodyState.OUT_OF_WORLD );
	}

	/**
	 * Exits a creature from the world.
	 * 
	 * @param _creatureBody the body of the creature
	 */
	private void exitCreature( CreatureBody _creatureBody )
	{
		killAgentBody( _creatureBody.getOwner() );
		_creatureBody.setState( CreatureBodyState.OUT_OF_WORLD );
	}

	/**
	 * Interface of an action effect on a world.
	 * 
	 * @author Tarik Belabbas
	 * 
	 */
	private interface WorldEffect
	{
		/**
		 * Applies the effect on an world.
		 */
		public void applyEffect();
	}

	/**
	 * Implementation of a movement effect on a world.
	 * 
	 * @author Tarik Belabbas
	 * 
	 */
	private class WorldMoveEffect implements WorldEffect
	{
		/**
		 * The agent body affected by the movement.
		 */
		private final CreatureBody	agentbody;

		/**
		 * The movement to apply on the body.
		 */
		private final Vector2i	movement;

		/**
		 * Parameterized constructor.
		 * 
		 * @param _agentBody the agent body affected by the movement
		 * @param _movement the movement to apply on the body
		 */
		public WorldMoveEffect( CreatureBody _agentBody, Vector2i _movement )
		{
			this.agentbody = _agentBody;
			this.movement = _movement;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * fr.utbm.gi.vi51.slizers.pw.environment.World.WorldEffect#doWork()
		 */
		@Override
		public void applyEffect()
		{
			move( this.agentbody, this.movement );
		}
	}

	/**
	 * Implementation of an world element destruction effect.
	 * 
	 * @author Tarik Belabbas
	 * 
	 */
	private class WorldDestroyEffect implements WorldEffect
	{
		/**
		 * The position of the element to destroy.
		 */
		private final Point2i	elementToDestroy;

		/**
		 * Parameterized constructor.
		 * 
		 * @param _elementToDestroy the position of the element to destroy
		 */
		public WorldDestroyEffect( Point2i _elementToDestroy )
		{
			this.elementToDestroy = _elementToDestroy;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * fr.utbm.gi.vi51.slizers.pw.environment.World.WorldEffect#doWork()
		 */
		@Override
		public void applyEffect()
		{
			setWorldElement( this.elementToDestroy, WorldElement.AIR );
		}
	}

	public Point2i getMapExit() {
		return mapExit;
	}
}
